/*
 * Copyright 2008 Google Inc.
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Based on other plugins in Cyrus SASL, whose license is:
 * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any other legal
 *    details, please contact  
 *      Office of Technology Transfer
 *      Carnegie Mellon University
 *      5000 Forbes Avenue
 *      Pittsburgh, PA  15213-3890
 *      (412) 268-4387, fax: (412) 268-7395
 *      tech-transfer@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


/*
 * "gvn_userdb" auxprop plugin
 *
 * This module implements a "filesystem-as-database" view of authn
 * data; it has the same backend as the Apache mod_authn_gvn_userdb;
 * see gvn.userdb Python module for details.
 *
 * The configuration directive "gvn_userdb_path" is used to set the
 * directory underwhich all authn is to be found (herein referred to
 * as "$dbdir").
 *
 * To use, put something like this in your configuration file (eg,
 * /usr/lib/sasl2/svn.conf):
 *
 *   pwcheck_method: auxprop
 *   auxprop_plugin: gvn_userdb
 *   gvn_userdb_path: /opt/svn/userdb
 *   mech_list: CRAM-MD5
 *
 * Password files are located at:
 *   $gvn_userdb_path/somehash(USERNAME)/USERNAME/password
 *
 * The format of these files is:
 *     <crypted_password>:<opaque>[:...]
 *
 * (This plugin only reads the opaque password.
 *
 * Regular digest-type files are located at:
 *     $dbdir/somehash(<username>)/<username>/realm_<realm>
 *
 * The format of this file is:
 *     <realm_hash>[:<opaque...>]
 *
 * "somehash()" is currently the first 3 (HASH_BYTES) characters of
 * the username, as in "mer" for "merlin".
 *
 * 3 letters gives a total of 17576 possible directories, although in
 * real life it'll be more around 5-10k due to the distribution of
 * letters in logins. This is under the 32,000 directory limit for
 * linux ext2 and ext3 filesystems, which is good.  This hash level
 * then gives us 100 to 500 files per directory for 1 million users,
 * which is fine, and still lets you scale up to around 10 million
 * users before you have to worry about big directories.
 *
 * Note that you should still use a filesystem that has directory
 * hashing (eg, the dir_index option in ext3).
 *
 * This module was created for the gvn project:
 *    http://code.google.com/p/gvn/
 * where you will find the tools admin-userdb and populate-userdb
 * as well as lib/gvn/userdb.py to administer the on disk database.
 *
 * This is written to be built under the Cyrus build system, but can
 * be compiled independently if you define BUILD_OUTSIDE_SASL.
 */

#ifndef BUILD_OUTSIDE_SASL
#include <config.h>
#endif

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>

#ifdef BUILD_OUTSIDE_SASL
#include <sasl.h>
#include <saslutil.h>
#include <saslplug.h>
#else
#include "sasl.h"
#include "saslutil.h"
#include "saslplug.h"
#endif

#include <ctype.h>

#include "userdb.h"

#ifndef MEMERROR
#define MEMERROR( utils ) \
    (utils)->seterror( (utils)->conn, 0, \
                       "Out of Memory in " __FILE__ " near line %d", __LINE__ )
#endif

typedef struct userdb_settings {
  const char *path;
} userdb_settings_t;

static void
userdb_auxprop_lookup(void *glob_context,
                      sasl_server_params_t *sparams,
                      unsigned flags,
                      const char *user_with_realm,
                      unsigned user_with_realm_len)
{
  const struct propval *which_props, *cur;
  const char *at_sign;
  char *username;
  userdb_settings_t *settings = glob_context;
  char buf[GVN_USERDB_MAX_LINE_LEN];

  if (!glob_context || !sparams || !user_with_realm)
    return;

  sparams->utils->log(NULL, SASL_LOG_DEBUG, "gvn_userdb plugin called\n");

  /* The 'user_with_realm' that we receive should actually be
   * username@realm, where 'realm' can be set in the svnserve.conf and
   * defaults to the repository's UUID.  (However, AFAIK, this should
   * be treated as untrusted user-provided data: we can't assume
   * anything about the format.)
   *
   * We parse it into the two pieces, though we don't actually do
   * anything with the realm.  (Note that a comment in sql.c there
   * implies that calling prop_get corrupts the "user_with_realm"
   * parameter, so we always copy it.) */

  at_sign = memchr(user_with_realm, '@', user_with_realm_len);

  if (at_sign)
    {
      int username_len = at_sign - user_with_realm;
      username = sparams->utils->malloc(username_len + 1);
      if (! username)
        {
          MEMERROR(sparams->utils);
          goto cleanup;
        }
      memcpy(username, user_with_realm, username_len);
      username[username_len] = '\0';
    }
  else
    {
      username = sparams->utils->malloc(user_with_realm_len + 1);
      if (! username)
        {
          MEMERROR(sparams->utils);
          goto cleanup;
        }
      memcpy(username, user_with_realm, user_with_realm_len);
      username[user_with_realm_len] = '\0';
    }

  /* Theoretically, SASL could be asking for a variety of different
   * types of secrets.  In practice it is always asking for
   * 'userPassword' or 'cmusaslsecretMECHNAME'.  I am pretty sure that
   * userPassword is the password itself, whereas
   * cmusaslsecretMECHNAME is a precomputed secret based on the
   * password.  For now, let's just stick with the userPassword
   * (SASL_AUX_PASSWORD) part. */

  which_props = sparams->utils->prop_get(sparams->propctx);
  if (!which_props)
    goto cleanup;

  /* So apparently this function can get called with
   * SASL_AUXPROP_AUTHZID as part of the flags argument or not.  If
   * it is, it should ignore any prop whose name begins with '*'; if
   * the flag is cleared, it should only pay attention to '*' props.
   * In either case, the real name of the prop starts after the '*'.
   *
   * We're *only* trying to handle SASL_AUX_PASSWORD
   * ("*userPassword").  So let's just skip completely if the flag is
   * set.
   */
  if (flags & SASL_AUXPROP_AUTHZID)
    goto cleanup;

  /* Loop over each of the properties that they're looking for. */
  for (cur = which_props; cur->name; cur++)
    {
      char *filename, *password;

      /* Is it the one property we know about? */
      if (strcmp(cur->name, SASL_AUX_PASSWORD) != 0)
        continue;

      /* If it's there already, we want to see if it needs to be
       * overridden.  (This is standard SASL auxprop plugin
       * boilerplate.) */
      if (cur->values && !(flags & SASL_AUXPROP_OVERRIDE))
        continue;
      else if (cur->values)
        sparams->utils->prop_erase(sparams->propctx, cur->name);

      sparams->utils->log(NULL, SASL_LOG_DEBUG,
                          "gvn_userdb plugin looking up the password of '%s'\n",
                          username);

      /* Actually look up the password in the filesystem. */
      {
        size_t len = gvn_userdb_password_filename_length(settings->path,
                                                         username);
        filename = sparams->utils->malloc(len);
      }
      if (!filename)
        {
          MEMERROR(sparams->utils);
          return;
        }
      gvn_userdb_password_filename(filename, settings->path, username);
      password = gvn_userdb_password(filename, buf);
      sparams->utils->free(filename);

      /* If we fail to find the value (ie, no such user), just don't
         call prop_set.  Otherwise send the password to SASL, and free
         it. */
      if (password)
        {
          sparams->utils->log(NULL, SASL_LOG_DEBUG,
                              "gvn_userdb plugin found password!\n");
          sparams->utils->prop_set(sparams->propctx, cur->name,
                                   password, strlen(password));
        }
    }

 cleanup:
  if (username)
    sparams->utils->free(username);
}

/* Free up our global settings. */
static void
userdb_auxprop_free(void *glob_context, const sasl_utils_t *utils)
{
  userdb_settings_t *settings = glob_context;

  if (!settings)
    return;

  utils->log(NULL, SASL_LOG_DEBUG, "gvn_userdb freeing memory\n");
  utils->free(settings);
}

static sasl_auxprop_plug_t userdb_auxprop_plugin = {
  0,                            /* Features */
  0,                            /* spare */
  NULL,                         /* glob_context */
  userdb_auxprop_free,          /* auxprop_free */
  userdb_auxprop_lookup,        /* auxprop_lookup */
  "gvn_userdb",                 /* name */
  NULL                          /* auxprop_store */
};

int
gvn_userdb_auxprop_plug_init(const sasl_utils_t *utils,
                             int max_version,
                             int *out_version,
                             sasl_auxprop_plug_t **plug,
                             const char *plugname __attribute__((unused)))
{
  userdb_settings_t *settings;
  int r;

  /* Make sure our parameters are kosher. */
  if (!out_version || !plug)
    return SASL_BADPARAM;
  if (max_version < SASL_AUXPROP_PLUG_VERSION)
    return SASL_BADVERS;
  *out_version = SASL_AUXPROP_PLUG_VERSION;

  /* Export our vtable. */
  *plug = &userdb_auxprop_plugin;

  /* Allocate and clear our settings. */
  settings = utils->malloc(sizeof(userdb_settings_t));
  if (!settings)
    {
      MEMERROR(utils);
      return SASL_NOMEM;
    }
  memset(settings, 0, sizeof(userdb_settings_t));

  /* Read in the 'gvn_userdb_path' setting. */
  r = utils->getopt(utils->getopt_context, "gvn_userdb", "gvn_userdb_path",
                    &(settings->path), NULL);
  if (r || !settings->path)
    {
      utils->log(NULL, SASL_LOG_ERR, "no gvn_userdb_path specified");
      utils->free(settings);
      return SASL_NOMECH;
    }
  utils->log(NULL, SASL_LOG_DEBUG, "gvn_userdb auxprop plugin with path '%s'\n",
             settings->path);

  /* Save our settings into the vtable. */
  userdb_auxprop_plugin.glob_context = settings;

  return SASL_OK;
}

/* In the Cyrus build system, this function gets autogenerated in
   gvn_userdb_init.c */
#ifdef BUILD_OUTSIDE_SASL
extern int sasl_auxprop_plug_init(const sasl_utils_t *utils,
                                  int maxversion, int *out_version,
                                  sasl_auxprop_plug_t **plug,
                                  const char *plugname)
{
  return gvn_userdb_auxprop_plug_init(utils, maxversion, out_version,
                                      plug, plugname);
}
#endif
